package com.atguigu.srb.core.service.impl;

import com.alibaba.excel.util.StringUtils;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.nacos.common.utils.CollectionUtils;
import com.atguigu.srb.base.util.Asserts;
import com.atguigu.srb.common.result.ResponseEnum;
import com.atguigu.srb.core.entity.*;
import com.atguigu.srb.core.entity.vo.BorrowerDetailVo;
import com.atguigu.srb.core.entity.vo.LendSearchVo;
import com.atguigu.srb.core.enums.LendStatusEnum;
import com.atguigu.srb.core.enums.TransTypeEnum;
import com.atguigu.srb.core.hfb.HfbConst;
import com.atguigu.srb.core.hfb.RequestHelper;
import com.atguigu.srb.core.mapper.DictMapper;
import com.atguigu.srb.core.mapper.LendMapper;
import com.atguigu.srb.core.service.*;
import com.atguigu.srb.core.util.*;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import io.swagger.annotations.ApiModelProperty;
import lombok.extern.slf4j.Slf4j;
import org.joda.time.DateTime;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.*;
import java.util.stream.Collectors;

/**
 * <p>
 * 标的准备表 服务实现类
 * </p>
 *
 * @author Atguigu
 * @since 2023-06-30
 */
@Service
@Slf4j
public class LendServiceImpl extends ServiceImpl<LendMapper, Lend> implements LendService {
    @Resource
    DictMapper dictMapper;
    @Resource
    private BorrowerService borrowerService;
    @Resource
    LendService lendService;
    @Resource
    UserAccountService userAccountService;
    @Resource
    UserInfoService userInfoService;
    @Resource
    TransFlowService transFlowService;
    @Resource
    LendItemService lendItemService;
    @Resource
    LendReturnService lendReturnService;
    @Resource
    LendItemReturnService lendItemReturnService;
    //带条件查询标的列表
    @Override
    public List<Lend> getLendLists(LendSearchVo lendSearchVo) {
        //获取条件vo对象里面的条件
        //创建wrappers构建器
        LambdaQueryWrapper<Lend> wrapper = Wrappers.lambdaQuery(Lend.class);
        //1.根据returnMethods构建条件构造器
        //1:等额本息 2:等额本金 3:每月还息一次还本 4:一次还本还息
        List<Integer> returnMethods = lendSearchVo.getReturnMethods();
        if(CollectionUtils.isNotEmpty(returnMethods)){
            wrapper.in(Lend::getReturnMethod,returnMethods);
        }
        //2.根据标的类型执行模糊查询
        //1:车易贷 2:房易贷 3:美容贷 4:学习贷 5:装修贷
        List<Integer> projectTypes = lendSearchVo.getProjectTypes();
        if(CollectionUtils.isNotEmpty(projectTypes)){
            //条件里面的标的的类型不为空的时候才来构建这个查询的条件
            wrapper.and(w -> {
                projectTypes.forEach(projectType -> {
                    switch (projectType){
                        case 1:
                            w.or().like(Lend::getTitle,"车易贷");
                            break;
                        case 2:
                            w.or().like(Lend::getTitle,"房易贷");
                            break;
                        case 3:
                            w.or().like(Lend::getTitle,"美容贷");
                            break;
                        case 4:
                            w.or().like(Lend::getTitle,"学习贷");
                            break;
                        case 5:
                            w.or().like(Lend::getTitle,"装修贷");
                            break;
                    }
                });
            });
        }

        //3.根据还款期数构建条件构造器 每个项目的条件之间是and链接
        //1:1月以下 2:1-3月 3:3-6月 4:6-12月 5:12月以上
        List<Integer> periodTypes = lendSearchVo.getPeriodTypes();
        if(CollectionUtils.isNotEmpty(periodTypes)){
            //进来这里面说明查询条件里面的还款期数不为空
            wrapper.and(w ->{
                periodTypes.forEach(periodType ->{
                    switch (periodType){
                        case 1:
                            w.or().lt(Lend::getPeriod,1);
                            break;
                        case 2:
                            //  1< ≤3
                            //内部是and链接 , 每个periodType之间是or链接
                            w.or(sub -> sub.lt(Lend::getPeriod,3).ge(Lend::getPeriod,1));
                            break;
                        case 3:
                            w.or(sub -> sub.lt(Lend::getPeriod,6).ge(Lend::getPeriod,3));
                            break;
                        case 4:
                            w.or(sub -> sub.lt(Lend::getPeriod,12).ge(Lend::getPeriod,6));
                            break;
                        case 5:
                            w.or().ge(Lend::getPeriod,12);
                            break;
                    }
                });
            });
        }

        //4.根据年华利率构建条件构造器
        //1:12%以下 2:12%-14% 3:14%-16% 4:16%以上 左边闭区间,右边开区间
        List<Integer> yearRateTypes = lendSearchVo.getYearRateTypes();
        if(CollectionUtils.isNotEmpty(yearRateTypes)){
            //进来这里面说明年化利率的查询条件不为空
            wrapper.and(w -> {
                yearRateTypes.forEach(yearRateType -> {
                    switch (yearRateType){
                        case 1:
                            //年化率 ≤ 12%
                            w.or().lt(Lend::getLendYearRate,12);
                            break;
                        case 2:
                            w.or(sub ->{
                                // 12 ≤ 年化率 <14%
                                sub.lt(Lend::getLendYearRate,14).ge(Lend::getLendYearRate,12);
                            });
                            break;
                        case 3:
                            w.or(sub ->{
                                // 14 ≤ 年化率 <16%
                                sub.lt(Lend::getLendYearRate,16).ge(Lend::getLendYearRate,14);
                            });
                            break;
                        case 4:
                            // 16 ≤ 年化率
                            w.or().ge(Lend::getLendYearRate,16);
                            break;

                    }
                });
            });
        }
        //根据条件查询lend lists集合
        List<Lend> list = this.list(wrapper);

        //判断一下,避免没有查询到内容,后面出现空指针的情况
        if(CollectionUtils.isNotEmpty(list)){
            //5.设置还款方式和状态到params的map中
            //5.1设置returnMethod到params中
            //lambda的方法引用
            list.forEach(this::setStatusAndReturnMethod);
        }

        //6.返回查询的结果
        return list;
    }

    private void setStatusAndReturnMethod(Lend lend) {
        String returnMethod = dictMapper.getDictNameByDictCodeAndValue("returnMethod", lend.getReturnMethod());
        lend.getParams().put("returnMethod",returnMethod);
        //5.2设置status到params中
        String status = null;
        switch (lend.getStatus()){
            case 0:
                status = LendStatusEnum.getMsgByStatus(0);
                break;
            case 1:
                status = LendStatusEnum.getMsgByStatus(1);
                break;
            case 2:
                status = LendStatusEnum.getMsgByStatus(2);
                break;
            case 3:
                status = LendStatusEnum.getMsgByStatus(3);
                break;
            case 4:
                status = LendStatusEnum.getMsgByStatus(4);
                break;
            case -1:
                status = LendStatusEnum.getMsgByStatus(-1);
                break;
        }
        lend.getParams().put("status",status);
    }

    //admin管理模块里面获取标的的列表
    @Override
    public List<Lend> getLendsList() {
        //查询获取所有的标的的详情 根据发布日期来降序排列
        List<Lend> list = this.list(Wrappers.lambdaQuery(Lend.class).orderByDesc(Lend::getPublishDate));
        //设置标的的status和returnMethod
        //setStatusAndReturnMethod(lend);
        list.forEach(this::setStatusAndReturnMethod);
        return list;
    }

    //根据id获取标的的详情
    @Override
    public Map<String, Object> getDetailById(Long lendId) {
        //根据lend id查询获取lend
        Lend lend = this.getById(lendId);
        //设置lend的status和returnMethod显示的内容到params中
        this.setStatusAndReturnMethod(lend);

        //根据lendId获取userId,然后根据userId获取到borrower的详情
        Long userId = lend.getUserId();
        //borrowerServiceImpl里面有根据userId获取borrower详情的方法,这里直接复用即可
        BorrowerDetailVo borrowerDetailVo = borrowerService.getDetailByUserId(userId);

        //设置lend和borrowerDetail到map中
        Map<String,Object> map = new HashMap<>();
        map.put("borrower",borrowerDetailVo);
        map.put("lend",lend);
        return map;
    }

    //放款管理,此为同步的方法,和之前和hfb异步的接口方法有点不一样
    @Transactional(rollbackFor = Exception.class)
    //下面的业务代码里面不要try catch异常,否则事务管理器无法捕获异常,也就不能起作用了
    @Override
    public void makeLoan(Long lendId) {
        //先根据传过来的lendId查询获得lend对象
        Lend lend = lendService.getById(lendId);

        //TODO 校验参数
        //1.lend的状态必须是募资中(其余状态无法也不能放款)
        Asserts.AssertTrue(lend.getStatus().intValue() == LendStatusEnum.INVEST_RUN.getStatus(),ResponseEnum.LEND_INVEST_ERROR);

        //2.(借款人)用户必须有绑定hfb账户
        Long userId = lend.getUserId();
        //下面的这个方法里面会校验用户是否有绑定汇付宝的账户
        UserInfo userInfo = userInfoService.checkUserInfoByUserId(userId);

        //3.用户不能是锁定状态
        Asserts.AssertTrue(userInfo.getStatus() == 1,ResponseEnum.LOGIN_LOCKED_ERROR);


        //TODO 准备汇付宝放款接口所需要的参数
        //agentId
        Map<String,Object> params = new HashMap<>();
        params.put("agentId", HfbConst.AGENT_ID);//给商户分配的唯一标识
        params.put("agentProjectCode",lend.getLendNo());//放款的项目编号
        //放款的单号
        String loanNo = LendNoUtils.getLoanNo();
        params.put("agentBillNo",loanNo);
        //商户手续费 之前生成标的的时候计算的是预期的收益,不能拿来直接用,因为有可能不是满标的状态放的款
        //需要根据实际的投资的金额再次计算一个平台的实际的收益  = 实际募资金额 * 月化利服务率 * 期数
        //月化服务利率
        BigDecimal monthRate = lend.getServiceRate().divide(new BigDecimal(12 * 100), 8, BigDecimal.ROUND_DOWN);
        BigDecimal mchFee = lend.getInvestAmount().multiply(monthRate).multiply(new BigDecimal(lend.getPeriod()));
        params.put("mchFee",mchFee);//商户手续费
        params.put("timestamp",RequestHelper.getTimestamp());//时间戳
        params.put("sign", RequestHelper.getSign(params));//签名

        //TODO 向hfb发起请求执行放款
        JSONObject res = RequestHelper.sendRequest(params, HfbConst.MAKE_LOAD_URL);
        //获取响应里面的参数
        String resultCode = res.get("resultCode").toString();
        BigDecimal voteAmt = new BigDecimal(res.get("voteAmt").toString());//放款的金额
        BigDecimal mchFee1 = new BigDecimal(res.get("mchFee").toString());

        //校验hfb放款的操作是否成功
        Asserts.AssertTrue(StringUtils.equals(resultCode,"0000"), ResponseEnum.HFB_MAKE_LOAN_FAIL);

        //TODO 更新标的的状态以及平台的收益
        //设置lend真实收益
        lend.setRealAmount(mchFee);
        //设置标的状态为还款中
        lend.setStatus(LendStatusEnum.PAY_RUN.getStatus());
        //设置放款的时间
        lend.setPaymentTime(new Date());
        //设置放款人id  这里admin暂时没有设置登录之类的,所以没有id,先用1代替
        lend.setPaymentAdminId(1L);
        //更新lend
        lendService.updateById(lend);

        //TODO 借款人相关
        //执行转账给借款人
        Long borrowerUserId = lend.getUserId();
        UserInfo borrower = userInfoService.getById(borrowerUserId);
        userAccountService.updateAmountAndFreezeAmountByUserId(voteAmt,new BigDecimal(0),borrowerUserId,new Date());
        //借款人相关的交易流水
        //充值交易状态0 未完成; 1 已完成; 2 失败; 3 resultCode异常; 4 代表验签失败; 5 代表充值金额异常
        String memo = "借款放款到账，编号：" + lend.getLendNo();
        transFlowService.saveTransFlow(voteAmt,borrower,loanNo,TransTypeEnum.BORROW_BACK,1,memo);


        //TODO 投资人相关 投资人不止一个
        //解冻并扣除投资人的资金
        List<LendItem> list = lendItemService.list(Wrappers.lambdaQuery(LendItem.class).eq(LendItem::getLendId, lendId));
        list.forEach(lendItem -> {
            //遍历,将每一个投资者的冻结金额改为0
            //投资者的id
            Long investUserId = lendItem.getInvestUserId();
            //投资者投资的金额
            BigDecimal investAmount = lendItem.getInvestAmount();
            userAccountService.updateAmountAndFreezeAmountByUserId(new BigDecimal(0),investAmount.negate(),investUserId,new Date());
            //增加投资人的交易流水 chargeNo要生成新的
            String transNo = LendNoUtils.getTransNo();
            String memo1 = "冻结资金转出，出借放款，编号：" + lend.getLendNo();
            transFlowService.saveTransFlow(investAmount,userInfoService.getById(investUserId),transNo,TransTypeEnum.INVEST_UNLOCK,1,memo1);
        });

        // 生成借款人还款计划和出借人回款计划
        this.generateRepayPlan(lend);

    }

    //借款人还款计划
    void generateRepayPlan(Lend lend){
        //生成还款计划列表
        List<LendReturn> lendReturnList = new ArrayList<>();
        //根据期数来进行循环
        for(int i = 1;i <= lend.getPeriod();i++){
            //创建还款计划对象
            LendReturn lendReturn = new LendReturn();
            //设置还款计划对象的属性
            //设置标的id
            lendReturn.setLendId(lend.getId());
            //设置借款信息id
            lendReturn.setBorrowInfoId(lend.getBorrowInfoId());
            //设置还款批次号
            String returnNo = LendNoUtils.getReturnNo();
            lendReturn.setReturnNo(returnNo);
            //设置借款人用户id
            lendReturn.setUserId(lend.getUserId());
            //设置借款金额amount 借款申请里面申请的总金额
            lendReturn.setAmount(lend.getAmount());
            //设置计息本金额  也就是实际借出去的钱(实际筹集到的钱)
            lendReturn.setBaseAmount(lend.getInvestAmount());
            //设置期数
            lendReturn.setCurrentPeriod(i);
            //设置年化率 记得年化率是放大了100倍来存储的
            lendReturn.setLendYearRate(lend.getLendYearRate());
            //设置还款方式
            lendReturn.setReturnMethod(lend.getReturnMethod());
            // TODO 本金principal 暂时空着
            // TODO 利息interest 暂时空着
            // TODO 本息total 暂时空着
            lendReturn.setFee(new BigDecimal(0));//放款的时候已经扣过手续费了
            //还款时指定的还款日期 lendStartTime时间的下一个月开始
            lendReturn.setReturnDate(new DateTime(lend.getLendStartDate()).plusMonths(i).toDate());
            //realReturnTime 实际发生的还款时间
            lendReturn.setOverdue(false);//是否逾期
            //overdueTotal 逾期金额
            //状态（0-未归还 1-已归还）
            lendReturn.setStatus(0);
            //是否最后一次还款
            //进来这里面说明是最后一期,也就是最后一期还款计划 如果是最后一期,那么last就是true,否则last就是false
            lendReturn.setLast(i == lend.getPeriod());
            //将本期的还款计划存到还款计划列表里面
            lendReturnList.add(lendReturn);
        }
        //循环结束,批量保存还款计划
        lendReturnService.saveBatch(lendReturnList);

        //准备一个map集合,key是period value是这个月对应的还款计划的id,用来和回款计划做绑定
        Map<Integer, Long> collect = lendReturnList.stream().collect(Collectors.toMap(LendReturn::getCurrentPeriod, LendReturn::getId));

        //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

        //TODO 获取还款计划列表,设置回款计划列表的本金,利息和本息和
        List<LendItem> lendItems = lendItemService.list(Wrappers.lambdaQuery(LendItem.class).eq(LendItem::getLendId, lend.getId()));
        //获取该标的下所有的回款计划列表
        List<LendItemReturn> returnItemListAll = new ArrayList<>();
        lendItems.forEach(lendItem -> {
            //获取的是当前的lendItem下对应的回款计划列表
            List<LendItemReturn> returnItemList = getReturnItemList(lendItem, collect, lend);
            //将获取到的当前lendItem下的回款计划添加到所有的回款计划的列表中
            returnItemListAll.addAll(returnItemList);
        });

        //~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

        //TODO 遍历还款计划lendReturnList 设置里面的每个回款计划的principal interest total
        lendReturnList.forEach(lendReturn -> {
            //取出回款计划列表中的每一个对象,过滤他们(过滤的条件是回款计划里面的还款计划id等于现在正在遍历的还款计划的id),然后组成新的list
            List<LendItemReturn> LendItemReturns = returnItemListAll.stream()
                    .filter(lendItemReturn -> lendItemReturn.getLendReturnId().equals(lendReturn.getId()))
                    .collect(Collectors.toList());
            //将上面过滤出来的集合的每一个对象的principal求和,设置给当前的lendReturn对象的principal属性
            BigDecimal totalPrincipal = LendItemReturns.stream().map(LendItemReturn::getPrincipal).reduce(BigDecimal.ZERO,BigDecimal::add);
            BigDecimal totalInterest = LendItemReturns.stream().map(LendItemReturn::getInterest).reduce(BigDecimal.ZERO,BigDecimal::add);
            BigDecimal total = LendItemReturns.stream().map(LendItemReturn::getTotal).reduce(BigDecimal.ZERO,BigDecimal::add);

            lendReturn.setPrincipal(totalPrincipal);
            lendReturn.setInterest(totalInterest);
            lendReturn.setTotal(total);
        });
        //批量更新还款计划列表
        lendReturnService.updateBatchById(lendReturnList);
    }

    //还款人回款计划 针对的是具体的某一个lendItem对象执行的操作,返回值是一个list,里面是一个个回款计划
    List<LendItemReturn> getReturnItemList(LendItem lendItem,Map<Integer,Long> map,Lend lend){
        //将每期要回的款放到一个map中,每期应得的利息也是这样
        Map<Integer,BigDecimal> interest = null;
        Map<Integer,BigDecimal> principal = null;
        //总的期数
        Integer period = lend.getPeriod();
        //投资的金额
        BigDecimal investAmount = lendItem.getInvestAmount();
        //年化率 记得除以100
        BigDecimal lendYearRate = lendItem.getLendYearRate().divide(new BigDecimal(100),8,BigDecimal.ROUND_DOWN);

        //根据还款方式获取每个月应得的本金和利息
        switch (lend.getReturnMethod()){
            case 1:
                interest = Amount1Helper.getPerMonthInterest(investAmount,lendYearRate,period);
                principal = Amount1Helper.getPerMonthPrincipal(investAmount,lendYearRate,period);
                break;
            case 2:
                interest = Amount2Helper.getPerMonthInterest(investAmount,lendYearRate,period);
                principal = Amount2Helper.getPerMonthPrincipal(investAmount,lendYearRate,period);
                break;
            case 3:
                interest = Amount3Helper.getPerMonthInterest(investAmount,lendYearRate,period);
                principal = Amount3Helper.getPerMonthPrincipal(investAmount,lendYearRate,period);
                break;
            case 4:
                interest = Amount4Helper.getPerMonthInterest(investAmount,lendYearRate,period);
                principal = Amount4Helper.getPerMonthPrincipal(investAmount,lendYearRate,period);
                break;
        }

        //每期的回款的本金和利息都准备好了,接下来要开始循环创建回款计划了
        List<LendItemReturn> lendItemReturnList = new ArrayList<>();
        //课件上使用的是map的循环,我觉得这里直接使用for循环效果是一样的,可读性还更高
        for (Integer i = 1; i <= period; i++) {
            LendItemReturn lendItemReturn = new LendItemReturn();
            lendItemReturn.setLendReturnId(map.get(i));//标的还款id
            lendItemReturn.setLendItemId(lendItem.getId());//标的项id
            lendItemReturn.setLendId(lend.getId());//标的id
            lendItemReturn.setInvestUserId(lendItem.getInvestUserId());//投资者的userId
            lendItemReturn.setInvestAmount(lendItem.getInvestAmount());//出借金额
            lendItemReturn.setCurrentPeriod(i);//当前的期数
            lendItemReturn.setLendYearRate(lend.getLendYearRate());//年化利率
            lendItemReturn.setReturnMethod(lend.getReturnMethod());//还款方式
            lendItemReturn.setFee(new BigDecimal(0));//手续费
            //还款时指定的还款日期(起息日往后推对应的期数月)
            lendItemReturn.setReturnDate(new DateTime(lend.getLendStartDate()).plusMonths(i).toDate());
            // lendItemReturn.setRealReturnTime();//TODO 暂时没有实际发生的还款时间
            lendItemReturn.setOverdue(false);//是否逾期
            lendItemReturn.setOverdueTotal(new BigDecimal(0));//逾期金额
            lendItemReturn.setStatus(0);//状态（0-未归还 1-已归还）
            //是否是一次还本还息(如果是这个,那么方式又有点不一样)
            if(lend.getReturnMethod() == 4){
                if(i.intValue() == period.intValue()){
                    //进来这里面说明是最后一个月
                    //进来这里面说明是一次还本还息的还款方式
                    lendItemReturn.setPrincipal(principal.get(1));//本金
                    lendItemReturn.setInterest(interest.get(1));//利息
                }else{
                    lendItemReturn.setPrincipal(new BigDecimal(0));//本金
                    lendItemReturn.setInterest(new BigDecimal(0));//利息
                }
            }else{
                //最后一个月的计算方式不一样
                if(i.intValue() == period.intValue()){
                    //进来这里面说明是最后一个月
                    BigDecimal totalPrincipal = lendItemReturnList.stream().map(LendItemReturn::getPrincipal).reduce(BigDecimal.ZERO, BigDecimal::add);
                    //本金 投入的本金减去之前期数本金的总和
                    lendItemReturn.setPrincipal(lendItem.getInvestAmount().subtract(totalPrincipal));
                    //计算前面期数的利息的和
                    // Optional<BigDecimal> reduce = lendItemReturnList.stream().map(LendItemReturn::getInterest).reduce((a, b) -> a.add(b));
                    // Optional<BigDecimal> reduce1 = lendItemReturnList.stream().map(LendItemReturn::getInterest).reduce(BigDecimal::add);
                    // BigDecimal reduce2 = lendItemReturnList.stream().map(LendItemReturn::getInterest).reduce(BigDecimal.ZERO, BigDecimal::add);
                    BigDecimal totalInterest = lendItemReturnList.stream().map(LendItemReturn::getInterest).reduce(BigDecimal.ZERO, BigDecimal::add);
                    //利息 预期的收益减去之前期数的收益的总和
                    lendItemReturn.setInterest(lendItem.getExpectAmount().subtract(totalInterest));
                }else{
                    //进来这里面说明不是最后一个月
                    lendItemReturn.setPrincipal(principal.get(i));//本金
                    lendItemReturn.setInterest(interest.get(i));//利息
                }
            }
            lendItemReturn.setTotal(lendItemReturn.getPrincipal().add(lendItemReturn.getInterest()));//本息
            lendItemReturnList.add(lendItemReturn);
        }
        //批量保存lendItemReturn到数据库中
        lendItemReturnService.saveBatch(lendItemReturnList);
        //将lendItemReturn返回
        return lendItemReturnList;
    }
}
